//******************************************************************
//******************************************************************
//**********          ANts Peer To Peer Sources        *************
//
// ANts P2P realizes a third generation P2P net. It protects your
// privacy while you are connected and makes you not trackable, hiding
// your identity (ip) and crypting everything you are sending/receiving
// from others.

// Copyright (C) 2004  Roberto Rossi

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

package ants.p2p.security.sockets;

import java.net.*;
import java.io.*;
import java.security.*;
import java.security.spec.*;
import javax.net.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.net.ssl.*;

import ants.p2p.*;
import ants.p2p.utils.addresses.InetAddressWatchdog;
import ants.p2p.utils.addresses.AddressServerThread;

import org.apache.log4j.*;

public class SecureServer implements Runnable{
  String KEYSTORE = "certs";
  char[] KEYSTOREPW = "serverkspw".toCharArray();
  char[] KEYPW = "serverpw".toCharArray();
  int serverPort = 443;
  ServerSocket serverSocket = null;

  static Logger _logger = Logger.getLogger(SecureServer.class.getName());

  public Thread serverThread;

  Ant n;

  public SecureServer(Ant n, int port) throws Exception{
    this.n = n;
    this.serverPort = port;
    this.serverSocket = this.createServerSocket();
    try{
      serverThread = new Thread(this);
      serverThread.setPriority(10);
      serverThread.start();
    }catch(Exception e){_logger.error("",e);}
  }

  public SecureServer(Ant n) throws Exception,
      NoSuchAlgorithmException,
      InvalidParameterSpecException,
      InvalidAlgorithmParameterException,
      InvalidKeyException {
    this.n = n;
    this.serverSocket = this.createServerSocket();
    serverThread = new Thread(this);
    serverThread.setPriority(10);
    serverThread.start();
  }

  public void listen() {
    //UPnP mapping
    this.n.uPnPMapping();
    while (!n.isDisconnected() && !this.serverSocket.isClosed()) {
      try {
        _logger.info(n.getShortId() + " :Server Listening...");
        Socket localSocket = this.serverSocket.accept();
        if (localSocket != null) {
          RequestsHandler handler = new RequestsHandler(this, localSocket);
          handler.start();
        }
      }
      catch (Exception e) {
        _logger.error("Secure Server Socket listen cycle: " + this.serverSocket.getLocalPort() + " ", e);
        this.n.disconnect();
      }
    }
    _logger.info("Secure Server Socket closed: "+this.serverSocket.getLocalPort());
  }

  public void run(){
    listen();
  }

  public ServerSocket getServerSocket(){
    return this.serverSocket;
  }

  private ServerSocket createServerSocket() throws Exception {
    // Make sure that JSSE is available
    Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
    // A keystore is where keys and certificates are kept
    // Both the keystore and individual private keys should be password protected
    KeyStore keystore = KeyStore.getInstance("JKS");
    keystore.load(this.getClass().getClassLoader().getResource(KEYSTORE).openStream(), KEYSTOREPW);
    // A KeyManagerFactory is used to create key managers
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    // Initialize the KeyManagerFactory to work with our keystore
    kmf.init(keystore, KEYPW);
    // An SSLContext is an environment for implementing JSSE
    // It is used to create a ServerSocketFactory
    SSLContext sslc = SSLContext.getInstance("TLSv1");
    // Initialize the SSLContext to work with our key managers
    sslc.init(kmf.getKeyManagers(), null, null);
    // Create a ServerSocketFactory from the SSLContext
    ServerSocketFactory ssf = sslc.getServerSocketFactory();
    // Socket to me
    SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(serverPort);
    // Return a ServerSocket on the desired port (443)
    return serverSocket;
  }
}

class RequestsHandler extends Thread{
  Socket localSocket;
  SecureServer caller;

  static Logger _logger = Logger.getLogger(RequestsHandler.class.getName());

  public RequestsHandler(SecureServer caller, Socket localSocket){
    this.localSocket = localSocket;
    this.caller = caller;
  }
  public void run(){
    try {
      int type = this.localSocket.getInputStream().read();
      if(type == 0){
        _logger.info("Connection type: 0");
        NewClientHandler handler = new NewClientHandler(this.caller, this.localSocket);
        handler.start();
      }else if(type == 1){
        _logger.info("Connection type: 1");
        AddressesRequestHandler handler = new AddressesRequestHandler(this.caller, this.localSocket);
        handler.start();
      }else if(type == 2 && caller.n.acceptTCPDirectConnections()){
        _logger.info("Connection type: 2");
        DirectNeighbourRequestHandler handler = new DirectNeighbourRequestHandler(this.caller, this.localSocket);
        handler.start();
      }else{
        localSocket.close();
      }
    }
    catch (IOException ex) {
      _logger.error("", ex);
      return;
    }
  }
}

class NewClientHandler extends Thread{
  Socket localSocket;
  SecureServer caller;

  static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

  public NewClientHandler(SecureServer caller, Socket localSocket){
    this.localSocket = localSocket;
    this.caller = caller;
    this.setPriority(5);
  }

  public void run(){
    try {
      String remoteAddress = localSocket.getInetAddress().getHostAddress();
      if (!InetAddressWatchdog.getInstance().allowedAddress(InetAddress.
          getByName(remoteAddress).getHostAddress())) {
        localSocket.close();
        throw new Exception("Address is not allowed: " + remoteAddress);
      }
      if (caller.n.getNeighboursNumber() < caller.n.getMaxNeighbours()) {
        localSocket.setKeepAlive(true);
        SecureServerSocketThread serverSocketThread = new
            SecureServerSocketThread(localSocket);
        if (serverSocketThread.isNewVersionDetected()) {
          this.caller.n.getPropertyChangeSupport().firePropertyChange(
              "newANtsVersionDetected", null,
              serverSocketThread.getNewerVersion());
        }
        if (!localSocket.isClosed()) {
          _logger.info(this.caller.serverSocket.getInetAddress().getHostAddress() +
                       ": Local time elapsed: " +
                       serverSocketThread.getTimeElapsed() + "[Thresold: " +
                       caller.n.getRateThresold() + "]");
          /*if (caller.n.getUnderRatedNeighbours() >=
              Math.floor(Ant.maxNeighbours * Ant.underRateConnections) &&
              serverSocketThread.getTimeElapsed() >= caller.n.getRateThresold()) {
            throw new Exception(this.caller.serverSocket.getInetAddress().
                                getHostAddress() +
                                ": Rejected neighbour cause it doesn't satisfy bandwith request: [" +
                                caller.n.getUnderRatedNeighbours() + "/" +
                                Math.floor(Ant.maxNeighbours *
                                           Ant.underRateConnections) + "]");
          }*/
          NeighbourAnt na = new NeighbourAnt(caller.n,
                                             localSocket.getInetAddress() +
                                             "",
                                             localSocket.getPort(),
                                             serverSocketThread.
                                             getRemoteServerPort(),
                                             serverSocketThread.
                                             getCipherEnc(),
                                             serverSocketThread.
                                             getCipherDec(),
                                             serverSocketThread.getSocket(),
                                             false,
                                             serverSocketThread.getTimeElapsed());
          try {
            caller.n.addNeighbour(na);
            na.start();
          }
          catch (Exception e) {
            na.terminate();
            na.setFailure();
            na.start();
            throw new Exception(e);
          }
          _logger.info(caller.n.getShortId() + " :Server added neighbour...");
        }
        else {
          _logger.info(caller.n.getShortId() + " :Rejected connection...");
        }
      }
      else {
        localSocket.close();
        _logger.info(caller.n.getShortId() + " :Rejected neighbour...");
      }
    }
    catch (Exception e) {
      _logger.error("New client handler: " + this.caller.serverSocket.getLocalPort() +
                    " ", e);
    }
  }
}

class AddressesRequestHandler extends Thread{
  Socket localSocket;
  SecureServer caller;

  static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

  public AddressesRequestHandler(SecureServer caller, Socket localSocket){
    this.localSocket = localSocket;
    this.caller = caller;
    this.setPriority(5);
  }

  public void run(){
    try {
      String remoteAddress = localSocket.getInetAddress().getHostAddress();
      if (!InetAddressWatchdog.getInstance().allowedAddress(InetAddress.
          getByName(remoteAddress).getHostAddress())) {
        _logger.info("Address is not allowed: " + remoteAddress);
        localSocket.close();
      }
      else {
        AddressServerThread serverThread = new AddressServerThread(
            localSocket);
        serverThread.start();
      }
    }
    catch (Exception e) {
      _logger.error("Addresses request handler: " + this.caller.serverSocket.getLocalPort() + " ", e);
    }
  }
}

class DirectNeighbourRequestHandler extends Thread{
  Socket localSocket;
  SecureServer caller;

  static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

  public DirectNeighbourRequestHandler(SecureServer caller, Socket localSocket){
    this.localSocket = localSocket;
    this.caller = caller;
  }

  public void run(){
    try {
      localSocket.setKeepAlive(true);
      localSocket.setSoTimeout(3 * 60000);
      TCPDirectServerThread tcpDirectServerThread = new TCPDirectServerThread(localSocket, caller.n);
      tcpDirectServerThread.setPriority(10);
      tcpDirectServerThread.start();
    }
    catch (Exception e) {
      _logger.error("Direct neighbour request handler: " + this.caller.serverSocket.getLocalPort() + " ", e);
    }
  }
}
